C++重点


一、定义类与对象

1、定义类

class classname
{
public:
  //公有数据成员、成员函数;
private:
  //私有数据成员、成员函数;
protected:
  //保护数据成员、成员函数;
};

一定一定注意末尾要有分号作为结束类的定义标志

数据成员、成员函数可以为空。

访问权限:

可访问性:

公有成员可访问性最强,所有东西都可以访问

保护的成员可访问性一般,在自己这个类和派生类中都能访问。(除了友元)

私有成员可访问性最弱,只有自己的类可以访问。(除了友元)

2、定义对象

class classname
{
public:
  //公有数据成员、成员函数;
private:
  //私有数据成员、成员函数;
protected:
  //保护数据成员、成员函数;
};

//上面已经定义了一个类classname,现在用这个类定义对象,如下:

classname object_name;//定义了一个名字叫object_name的类的类型为classname的对象

3、类中成员的定义

类中的变量和普通变量定义一模一样,但是不能赋初值。

类中的函数需要在类内申明,类外定义(和一般函数申明也一样)。(除了友元函数)一般来说,函数要写在公有部分。类外的定义要注意函数的名字

class classname
{
public:
  int name (int,int);//函数类内申明
  //公有数据成员、成员函数;
private:
  //私有数据成员、成员函数;
protected:
  //保护数据成员、成员函数;
};


int classname::name (int para1,int para2)//函数类外定义
{
  //......
}

特例:内联函数直接放在类中定义就行 可以不申明inline,编译器会把类中定义的直接编译为内联函数

4、this指针

this指针是一个隐藏的指针,不用定义就会自动产生,专门用于类的成员函数。当一个对象的成员函数调用这个函数,这个对象的首地址传给this指针,所以就可以在函数内使用this->member来访问这个对象的member成员。

5、成员的访问

访问对象的格式为:对象.数据成员

访问classname类的A对象中的一个数据health,就是

classname A;//定义一个对象
int a=A.health;//读取对象A的health成员赋值给a
A.health=100;//把100写入对象A的health成员地址中

再次说明:在主函数中只能访问到公有成员

二、类的成员

构造函数是这个对象一建立起来就会运行的函数。析构函数是这个对象一删除就会运行的函数。这两个函数都不能返回值。

1、构造函数

构造函数可以有参数,所以可以重载,构造函数的名字要和类名相同

class classname
{
public:
  classname();//构造函数
  classname(int);//构造函数重载一号
  classname(int,int);//构造函数重载二号
  classname(double);//构造函数重载三号
  //公有数据成员、成员函数;
private:
  //私有数据成员、成员函数;
protected:
  //保护数据成员、成员函数;
};

和普通函数一样,构造函数也是类内申明,类外定义,但是不用说明返回类型(因为编译器知道这是构造函数,不会有返回值)

classname::classname ()//函数类外定义
{
  //......
}

构造函数用途很多,比如输出内容,让编写者知道这个类在这时候建立起来了

classname::classname ()
{
  cout<<"这个类现在建立了哦";
}

一旦用classname这个类来构造一个对象就会输出“这个类现在建立了哦”。

构造函数最常见的用途是给类中的元素赋初值,比如下面是某地警察的犯罪记录次数系统:

class fanzuicishu
{
public:
  fanzuicishu();
  fanzuicishu(string,int);
private:
  string name;
  int times;
};

fanzuicishu::crime ()
{
  cout<<"您搁这输入空气呢?";
}

fanzuicishu::crime (string input_name,int input_times)
{
  name=input_name;
  times=input_times;
}

当主函数中有:

fanzuicishu zs;

zs();

就会输出:您搁这输入空气呢?

如果这样:

fanzuicishu zs;

zs(zhangsan,20);

那么zs这个对象的数据成员name就会被赋值zhangsan,times会被复制20;这样就不用新开一个函数来单独修改新开对象zs的name和times。

这样一来,张三的名字(zhangsan)和犯罪次数(20次)在zs这个对象一建立的时候就被写进了警察局的系统里面。(当然现实中肯定没这么低级)

拷贝构造函数

将构造函数重载为可以把数据成员复制到新的一个对象的类上面,比如某个游戏中一个人有金钱和生命两个数据:

class classname
{
public:
  classname();//默认构造函数
  classname(int money,int health);//重载构造函数为开辟一个人的金钱和生命的内存空间并初始化,定义略
  classname(const classname &,int =1,int =1 );//重载为拷贝构造函数,和默认构造函数的区别就是有后两个参数,使编译器知道你在调用这个重载函数而不是默认构造函数,定义在下面

private:
  int money;
  int health;
protected:

};

classname::classname(const classname &A,int x=1,int y=1)//拷贝构造函数定义
{
  money=A.money;//复制金钱
  health=A.health;//复制生命
}

...//其他函数定义略

int main()
{
  classname A(100,10);//A的人有金钱100money,有生命10health
  classname B(A,0,0);//利用三个参数使编译器知道你在调用拷贝构造函数,新开对象B这个人,并且把A这个人的金钱,生命复制给B
}

2、析构函数

析构函数在类被删除的时候运行,一般用于告诉程序员,这个对象消失了,或者清除类中开的内存。和构造函数一个道理,但是不能加入参数。

析构函数也是类内申明,类外定义。格式为:同构造函数,与类名字一样,前面加上:~,如下:

class classname
{
public:
  classname();//构造函数
  ~classname();//析构函数
 
private:

protected:

}

注意:析构函数不加参数,所以不能重载

3、常数据成员

就是和平时都常数一样,前面加const就好,直接在类内说明时定义值即可。

4、常对象

申明对象时,前面加上const,常对象只读

class classname
{
public:
  ...
  private:
  ...
protected:
};
const classname A(1,2);//申明常对象,常对象只能调用构造函数赋初始值,一旦赋值,不能再改

5、常成员函数

常成员函数不能修改类中的数据成员,只能读

函数申明时和定义时,参数表的后面加上const,其他和普通成员函数一样

class classname
{
public:
  int function1(int,int) const;//末尾加const
private:
  ...
protected:
};
int classname::function1(int parameter1,int parameter2) const //定义后面加const
{
  ...
}

6、静态成员

静态成员是所有这个类的对象所公有的

一个例子

比如十个人拥有自己的属性,那么这十个人就是属于人这个类的十个对象,每个对象有自己的属性,但是这十个人共用一张银行卡,这个银行卡里面面的钱数量受到所有人的控制。假设账户原来有1000块,第一个人把银行卡花了100块,账户里剩下900块,900由这十个人共享,不属于任何一个人。

上面那个例子中,人是类,每个人是属于类的对象,而银行卡的钱数量是静态数据成员。

类内申明静态成员时,前面加上static,类外定义不必再说明static。静态成员不属于任何一个对象,是公共的。

静态成员也是在类外定义,不初始化默认为0,初始化需要用类名初始化而不是某个对象(这也说明静态成员不属于任何一个对象)。

静态成员详情

静态成员在程序开始运行时就分配内存空间(分配空间初始化值为0),程序结束才释放空间,所以一直存在,不必在主函数开始运行后(或者定义对象之后)才赋值。

class classname
{
public:
  static int money;//静态成员:钱的数量
private:
  ...
protected:
};

 int classname::money=1;//初始化用类的名字,不是对象,静态成员不初始化默认为0,不必在主函数中赋值

int main()
{
  classname object;
 
}

7、静态成员函数

静态成员函数只能访问类的静态数据成员,不能用于其他用途

class classname
{
public:
  static int money;//静态成员:钱的数量
  static int get_money(int);//静态成员函数:获取金钱数量
  static int add_money(int);//静态成员函数:增加金钱数量
private:
  ...
protected:
};

int classname::get_money(int the_money)
  {
    ...
  }
int classname::add_money(int the_money)
  {
    ...
  }

int main()
{
  ...
}

静态成员函数也是类内申明static类外不必static

三、友元

1、友元的性质

类classname的友元可以直接访问classname这个类的所有数据成员,包括私有。

友元可以是普通函数,可以是成员函数,也可以是另外一个类

类classname的友元可以直接访问classname这个类的所有数据成员,包括私有。

友元不具有对称性和传递性,比如B函数是A的友元,B可以使用A的所有成员,但除非特殊说明,A不可以用B的成员。A是B的友元,B是C的友元,但A不能用C中的数据成员

2、友元的申明与定义

说明函数f是类A的友元,直接在类A的f函数前面加上friend

class A
{
public:
  ...
  friend void f();//申明f是类A友元
private:
  ...
  int the_thing_he_knows ;
protected:
  ...
};

这样f就可以直接访问类A的the_thing_he_knows

在定义的时候,不需要void A::f()因为友元不属于这个类,把f放在类A中并且加上friend只是为了说明f是类A的友元

void f()//外部定义不需要表明f在A中,也不需要说明friend,直接当有访问权限的普通函数定义。
{
  cout<<the_thing_he_knows;
}

友元类同理,申明B是A的友元类,则类A中所有东西类B可以随便使用

四、类的继承与派生

1、有关继承和派生

关于类继承和派生的一个比喻

继承和派生其实是一个东西的两种说法

儿子继承了爸爸的基因,爸爸派生出儿子,差不多这个意思

类B继承类A,则类B中除了有从类A中继承过来的成员,还有自己的成员。而A没有B的成员。

不恰当的比喻:爸爸A把基因全部传给儿子B,儿子B有爸爸A全部基因,自己还额外出现一些基因。爸爸A没有儿子B新出现的基因。

另外一个比喻

类A:植物

类B:低等植物

类C:高等植物

那么B、C两个类是由A派生出来的,继承了A的属性。A有点属性,B、C都有。

植物有属性:由细胞组成,那么其派生类B、C继承了这个属性,由细胞组成

但是高等植物C有植物A没有的属性,低等植物B也有植物A没有的属性。

在这里,植物类A是基类,B、C是派生类

被继承的类(爸爸)相对于继承的类(儿子)叫做基类,继承的类(儿子)叫做派生类。

用图表示:箭头由派生类指向基类,如下图,A是基类,B、C是A的派生类

继承关系是相对的,比如下图

A是B的(直接)基类,A是C的(间接)基类,B是C的基类

2、继承的访问权限

继承总共有三类:

  • 公有继承
  • 私有继承
  • 保护继承

继承规则:

继承类型 继承前(基类)可访问性 继承后(派生类)可访问性
公有继承 公有 公有
保护 保护
私有 不可继承
私有继承 公有 私有
保护 私有
私有 不可继承
保护继承 公有 保护
保护 保护
私有 不可继承
对上表的理解

表看起来复杂,实则有规律可循

私有成员不能被继承:前文说过,类的私有数据只有在类内可以访问,在类外和派生类都不能访问,所以以下都抛开基类私有成员

公有继承下,公有还是公有,保护还是保护

保护继承下,全部变成保护

私有继承下,全部变成私有

一般程序都是用公有继承,保护继承和私有继承很少用

3、继承的写法

先构造一个植物类

class plants
{
public:
...
private:
...
protected:
...
};

然后把plants作为基类,构造高等植物和低等植物派生类

class higher_plants:public plants//*
{
public:
...
private:
...
protected:
...
};

class lower_plants:public plants//*
{
public:
...
private:
...
protected:
...
};

就是像打*号的地方这样,在类名字后面跟上继承类型public和基类的名字plants

派生类可以有多个(直接)基类,如果一个派生类有不止一个(直接)基类

class higher_plants:public plants, public type_1, private type_2
{
public:
...
private:
...
protected:
...
};

像这样,写出所有继承类型直接基类的名字并用逗号隔开

4、访问申明

恢复继承过程中被降低可访问性的成员的可访问性

格式:

class plants
{
public:
  string set_name(string name);//设置植物名字
  int set_height(int value);//设置植物高度
  int set_radius(int value);//设置切面半径
  int count;//植物数量
...
private:
  string name;//植物名字
...
protected:
  double height;//植物高度
  double radius;//植物切面半径
...
};

比如,原本基类plant中公有成员count,在私有继承中count原本会变成私有。特地申明一下就恢复count为公有成员

申明的注意事项:

  • 私有成员不参与任何派生类的活动(除非特殊说明)。

私有变量height和radius无法被继承,更不用说申明了。

  • 申明时仅仅申明名字(附加作用域)。如果是函数,返回类型和参数都不用写;如果是变量,数据类型不用写。只用写出 “作用域::名字”(可以在作用域前面加上using)
class higher_plants:public plants, public type_1, private type_2
{
public:
  plants::set_name;//直接把函数名(带上作用域)摆在这里
  using plants::count;//直接把变量的名字摆出来(using可省略)
...
private:
...
protected:
...
};
  • 基类和派生类中访问申明的成员必须同一可访问性。(这一点有争议,我自己试了在public和private中可以自由改变)

count在基类是公有变量,申明后必须还是公有。不能申明完就变成保护了。

对于同名的重载函数(一般人都不会碰到这种情况)

(1)两个函数都在基类中,而且在同一访问域,两个申明都有效。

f() f(int)两个函数都是公有的,申明完都变成公有

(2)两个函数都在基类中,然而不在同一访问域,不能访问申明。

f()公有 f(int)私有,申明有歧义,不能申明

(3)一个函数在基类中,一个函数在派生类中,不能访问申明。

f()在基类A中,f(int)在A的派生类A'中,申明有歧义,不能申明

5、重名的成员

1、数据成员重名

基类中有height为名字的数据,派生类中又有定义一个叫height名字的数据。两个数据会分别建立存储空间,相当于两个不同的变量。

不写作用域默认为该类的成员,如果要弄到基类,需要标注作用域(作用域::成员)

class person
{
public:
  int age;//人的年龄
...
private:
...
protected:
...
};

class student:public person
{
public:
  int age;//学生的年龄
...
private:
...
protected:
...
};

int main()
{
  person me;
  student he;
  me.age=18;//person类的age
  he.age=18;//student类的age
  me.person::age=18;//person类的age
}
2、成员函数重名

同名的成员函数一个位于基类,一个位于派生类,派生类会把基类的屏蔽掉(而不是会寻找重载)。

class person
{
public:
  void get_age();//获得人的年龄
  void get_age(int rank);//获得第n个人的年龄,函数重载
...
private:
...
protected:
...
};

class student:public person
{
public:
  void get_age(int,int);//获得学生的年龄
...
private:
...
protected:
...
};

int main()
{
  student ten_students;
  ten_students.get_age();//错误,student类中的get_age(int,int)屏蔽了person中的get_age(),找不到对应重载函数,编译错误
  ten_students.get_age(5);//错误,student类中的get_age(int,int)屏蔽了person中的get_age(int),找不到对应重载函数,编译错误
  ten_students.person::get_age();//正确,指定基类person为作用域
  person five_persons;
  five_persons.get_age();//正确
  five_persons.get_age(int);//正确
}

被屏蔽的基类函数要通过作用域来访问(作用域::成员)

6、派生类中的静态成员

派生类直接和基类共享该静态成员(除非静态成员私有)

7、基类的初始化

派生类从基类继承的数据如果要通过构造函数初始化,需要调用基类的构造函数给这些继承过来的数据赋初值。

下面讲一下初始化列表

在构造函数后加上“:data(value)”,然后用逗号隔开,构成初始化列表,可以给data这个数据初始化赋值value。初始化列表必须加在函数的定义处,初始化是按照顺序来的,先传参数,由初始化列表从前到后赋值,最后再执行函数体。


class person
{
public:
  int a;
  int b;
  int c;
  person(int age):a(age),b(age),c(a){};//把age赋给a,b。把a赋给c,并且直接把函数当内联函数定义
  person(int,int);
...
private:
...
protected:
...
};

person::person(int age,int rank):a(age),b(age),c(a)//初始化列表紧跟定义
{
  ...
}

初始化列表中还可以传参数调用基类的构造函数。


class person
{
public:
  int age;
  int height;
  person(int age_1,int height_1){age=age_1;height=height_1};
...
private:
...
protected:
...
};

class student:public person
{
public:
  int weight;
  student(int age_1,int height_1,int weight_1):person(age_1,height_1),weight=weight_1{};
}
//调用基类构造函数person,同时还给weight赋值

上面age_1,height_1,weight_1作为参数传入派生类student的构造函数,然后调用基类person的构造函数person,将参数age_1,height_1传入,如此一来,派生类的age,height就赋值完成。

注意:基类早于派生类构造,晚于派生类释放。所以构造一个派生类对象,在调用构造函数之前,会进行其基类构造函数的调用。而在基类调用析构函数总是在派生类之后。

8、虚继承

如果发生这种情况

B、C继承A,D继承B、C。A中一个变量base在B、C中都存在,那么在D中会存在两个base,一个从B继承来,一个从C继承来。

这两个继承过来的base对应两个不同的存储空间,所以他们两者是独立的。

这样会引起歧义,D.base到底是哪一个?

于是我们申明虚继承,让原本是B::base和C::base的地方变成指针,指向同一个D::base,这样就消除了歧义,使得D的base唯一

申明时,在继承类型和基类之 前加上"virtual"

class D:virtual public B, virtual public C
{
public:
...
private:
...
protected:
...
};

类的运算符重载

引入

实际上,c++的库中已经对很多符号进行重载。

比如+号,当a+b时,编译器会根据a的类型来匹配相应的+号的重载类型

当a是int,+号重载为“整型的+”,即把两个int相加

当a是double,+号重载为“浮点型的+”,即把两个double相加

一、重载原则

1、不改变运算符优先级,包括优先结合性

2、不改变操作数个数

3、语义基本不变

解释

+号重载之后的优先级总不能高于×号吧?(不改优先级)

+号重载之后总不会只对一个数想加吧?(不改操作数个数)

+号重载之后总不会解释是-号吧?(语义不能太大改变)

能够重载的运算符
+ - * /
% ^ & |
~ ! = <
> += -= *=
/= %= ^= &=
|= << >> >>=
<<=< td> == != <=< td>
>= && || ++
-- ->* ' ->
[] () new delete
不能重载的运算符
. .* :: ?: sizeof

二、重载语法


  <function_type> <classname>::operator <sign>(parameter)
  {
    ......
  }

举个例子

class location//位置类,包含横纵坐标两个属性
{
public:...
private:
  int x;//横坐标
  int y;//纵坐标
};

void location::operator ++(int &x,int &y)//重载符号++
{
  x++;
  y++;
}

上面的例子把++作为重载符号,让一旦有location类的对象调用这个重载的符号++就调用函数,使得横纵坐标都+1。

三、重载符号在主函数的使用

对于上面位置类location的++重载

  int main()
  {
    location point(1,2);//设置point x=1,y=2
    point++;//point是location类的对象,使用了重载后的++,调用函数,使得x++,y++,最终结果是x=2,y=3
  }

方法就是直接在主函数中对象后面接上重载的符号

四、成员函数重载符号与友元函数重载符号

成员函数和友元函数用来重载符号虽然功能大致是相同的,但是调用重载的方式不一样,导致对于单操作数符号没什么问题,但对于双操作数符号就有可能出问题了。有必要指出,以下都是对双操作数符号解释

1、什么是双操作数符号?

+-×÷号有一左一右两个数,对两个数进行操作,称为双操作数符号(二元运算符),符号左边的叫左操作数,右边的叫右操作数。++,--只对一个数操作,所以称为单操作数符号(一元运算符)。

2、成员函数重载符号

成员函数重载符号,符号的左右操作数地位不想等,左边用于调用自身对象的成员函数,右边用于作为参数传入

成员函数重载的符号在被使用时,由左操作数调用函数,举个例子,空间向量

class the_vector//空间向量类,包含x,y,z
{
public:
  the_vector(int,int,int);
  the_vector operator+(the_vector,the_vector);
private:
  int x;
  int y;
  int z;
protected:
};

the_vector::the_vector(int x1,int y1,int z1)//构造函数设置空间向量
{
  x=x1;
  y=y1;
  z=z1;
}

the_vector the_vector::operator+(the_vector a,the_vector b)//重载为向量+号,使a向量和b向量相加,
{
  the_vector sum;
  sum.x=a.x+b.x;
  sum.y=a.y+b.y;
  sum.z=a.z+b.z;
  return sum;
}

int main()
{
  the_vector m(1,1,1);//定义m向量为(1,1,1)
  the_vector n(2,1,3);//定义n向量为(2,1,3)
  p=m+n;//p直接把两个向量相加,得到(3,2,4)
}

在上面的例子中,m+n这个操作进行的时候,由于这是成员函数重载+号,所以+号由m(左操作数)调用,所以在匹配+的重载会使用和m(左操作数)相同的类型的+号(即重载的向量+号)

所以m调用重载+函数实际上就是:m.operator+(n)

相当于把n作为参数传入对象m的成员函数

the_vector m(1,1,1);//定义m向量(1,1,1)
m+1;//等价于下面的写法
p=m.operator+(1);//调用m的成员函数,把1作为参数传入,最终得到p=(2,2,2)

但是如果

the_vector m(1,1,1);//定义m向量(1,1,1)
1+m;

就错了

左操作数是1,编译器会自动匹配整型的+号,然而右边不是整型而是一个类,就错了。

3、友元函数重载符号

使用友元函数来重载符号可以避免这种左右操作数地位不对等的问题

友元函数重载符号的左右两个操作数的地位都是对等的,都是作为参数传入

为什么?

前面说过,友元函数不属于类。所以就不存在成员函数的由左操作数调用对象的问题,两个操作数都变成参数传入友元函数

例子

class the_vector//空间向量类,包含x,y,z
{
public:
  the_vector(int,int,int);
  friend the_vector operator+(the_vector,the_vector);
private:
  int x;
  int y;
  int z;
protected:
};

the_vector::the_vector(int x1,int y1,int z1)//构造函数设置空间向量
{
  x=x1;
  y=y1;
  z=z1;
}

the_vector operator+(the_vector a,the_vector b)//友元重载为向量+号,使a向量和b向量相加,
{
  the_vector sum;
  sum.x=a.x+b.x;
  sum.y=a.y+b.y;
  sum.z=a.z+b.z;
  return sum;
}

int main()
{
  the_vector m(1,1,1);//定义m向量为(1,1,1)
  p=m+1;//等价于operator+(m,1)
  p=1+m;//等价于operator+(m,1)
}

这样子就正确了,m和1都是参数,地位同等,满足交换律,其中1会自动被转换成the_vector类(1,1,1)

注意:友元函数不能使用= () [] ->重载

4、几个特殊的运算符

(1)++和--

cpp规定,前置++是一元运算符,后置++是二元运算符。

为什么要这么规定?

用于区分前置的++和后置的++,程序员想让前置的++和后置的++有不同的作用,所以要让编译器知道要调用哪个

举个例子,我想让前置++先自增再赋值,后置++先赋值后自增

class the_vector
{
public:
friend the_vector operator++(the_vector&);//前置++,一元运算符
friend the_vector operator++(the_vector&,int);//后置++,二元运算符,多了个int
friend void operator+(the_vector&,the_vector);//把后者加到前者
friend void operator-(the_vector&,the_vector);//把前者减掉后者
...
private:
...
protected:
...
};
void operator+(the_vector &A,the_vector B)
{
  A.x+=B.x;
  A.y+=B.y;
  A.z+=B.z;
}
void operator-(the_vector &A,the_vector B)
{
  A.x-=B.x;
  A.y-=B.y;
  A.z-=B.z;
}
the_vector operator++(the_vector &A)//前置++
{
  A+1;//先自增
  return A;//再返回自增之后的数
}
the_vector operator++(the_vector &A,int x=1)
{
  A+1;//先自增
  return (A-1);//再返回自增之前的数
}

int main()
{
  the_vector m(1,2,3);
  the_vector m(1,2,3);
  p=++m;
  q=n++;
}

结果:p=(2,3,4),q=(1,2,3)

m自增之后的值赋给p,n自增之前的值赋给q

而编译器区分前置和后置的++符号就是通过参数的个数不同来区分应该调用哪个函数。

后置的++定义中的int x=1实际上就是个“伪参数”,作用仅仅是区分重载

(2)=赋值运算符的重载

“=”用于对象的复制,只能用成员函数重载,因为赋值是基于对象操作的,友元不属于类或者对象

重载赋值号“=”和拷贝构造函数的区别:拷贝构造函数在新建对象时进行赋值和初始化,重载赋值符号在作用域内任何时候都可以赋值给对象的数据成员

格式如下:

class type
{
public:
  type & operator=(type);//“=”号赋值的构造函数重载
...
private:
  int x;
  int y;
...
protected:
...
};

type & type::operator=(type A)
{
  this->x=A.x;
  this->y=A.y;
  return *this;//返回this指针所指的对象,就是当前对象
}

int main()
{
  
}
为什么要返回引用?(返回值类型为"type &")

返回引用可以连续调用重载的“=”进行赋值。A1=A2=A3...=An可以连续这么赋值,而且其中的所有“=”都是自己重载的版本

如果不返回引用就不能使用自己重载版本的A1=A2=A3...=An这么连续赋值.这样"="都是cpp自带的赋值方法

既然cpp自带赋值功能为什么还要自己重载?请往下看

深复制和浅复制

cpp自带的赋值的方法就称为“浅复制”,因为有时候自带的赋值方法会翻车,举个例子

某一个类,其中有一个数据成员是指针,现在对这个类新开一个对象A,A的指针用于指向存储A的某个数据的内存。现在用cpp版本的“浅复制”,复制出一个B,这个B的指针也只是简单的从A那里复制过来,所以A和B的这个指针指向同一片内存

这样导致A和B的指针共同修改A的某个数据。

然而我并不想这样,我想给B单独开一个储存这个数据的空间,然后让B的指针指向这个空间才对。这样需要“深复制”

深复制就是人为进行复制,把用cpp版本复制会出问题的部分单独拎出来,单独进行相关操作。

无论是复制构造函数,还是重载赋值号,都是一个重要的问题。(只不过一般不会碰到这种情况)

(3)流插入和提取运算符>>和<<
class the_vector
{
public:
  friend ostream & operator<<(ostream &,const the_vector&);//流提取<<重载
  friend istream & operator>>(istream &,the_vector&);//流插入>>重载
...
protected:
...
private:
  int x;
  int y;
  int z;
};

ostream & operator<<(ostream &output,const the_vector& a)
{
  output<<a.x<<"  "<<a.y<<"  "<<a.z;
  return output;
}
istream & operator>>(istream &input,the_vector&a)
{
  input>>a.x>>a.y>>a.z;
  return input;
}

int main()
{
  the_vector m;
  cin>>m;
  cout<<m;
}

这样就实现了cin>>m直接对对象m输入对象的内容,cout<< m 直接输出对象的内容

流插入和流提取必须重载为友元函数。

参数中ostream加上const是为了保险一点防止被修改

其中的istream和ostream是IO类中的东西。

返回类型为ostream &,就是把output这一段输出流中的东西插入到ostream输出流中,而函数体中就是把x,y,z插入output流中,istream同理。

必须要返回对象的引用 &,这样才能继续参与主函数中的cout<< 的运算。

5、类类型的转化

(1)使用构造函数转化

隐式转化:在无形之中转化,例子如下

class complex//复数类
{
public:
  complex(int rea,int ima=0){real=rea;image=ima};//构造函数
...
protected:
...
private:
  int image;
  int real;
};
void function(complex)
{
  ...
}
int main()
{
  complex x=3;//赋值时把3被代入构造函数转化
  x+5//算术运算时5被代入构造函数转化
  function(2);//作为参数时2被代入构造函数转化
}

显式转化:直接调用构造函数转化complex x=complex(3);

(2)类型转化函数

格式如下:

class complex
{
public:
  complex(int rea,int ima=0){real=rea;image=ima};//构造函数
  operator int();//类型转化函数
...
protected:
...
private:
  int image;
  int real;
};
complex::operator int ()
{
  return image;
}
int main()
{
  complex x;
  cout<<int(x);//主函数中调用类型转化函数
}

类的运算符重载

一、联编

1、静态联编

在程序正式开始之前的编译等操作,又称早期匹配

比如函数重载,根据参数不同和返回类型不同匹配就是静态联编过程。

2、动态联编

在程序开始后,运行时进行,又叫晚期联编。比如switch语句,if语句,在早期匹配时无法得到确定结果。

3、类指针的关系

四种情况

  • 基类指针指向基类
  • 派生类指针指向派生类
  • 基类指针指向派生类
  • 派生类指针指向基类

前两种显然没问题,看后两种

(1)基类指针指向派生类

基类指针若指向派生类,只能访问从基类继承的数据成员。如果要访问派生类独有的,需要强制类型转化为派生类指针。

强制类型转化,下面例子中:把p强制转化成type2(派生类)指针类型然后访问派生类中独有的函数

((type2 *)p)->show();

(2)派生类指向基类

派生类指向基类就必须要强制类型转换了

4、类对象的转化

派生类对象通过强制转化变成基类会丢失派生类中的特有数据。基类不能向派生类转化。

对象的转化可以使用成员函数的this指针(type2)(*this)把this指针所指对象转化为type2类

二、虚函数和动态联编

1、虚函数与基类指针

为什么要有虚函数这个东西?

如果一个基类A指针p指向派生类B,而且基类A和派生类B同时有同名函数f,由于该指针是基类A指针,所以会在派生类B中调用基类A的函数f,而不会调用派生类中的函数f

所以如果要通过基类指针访问派生类中的重名函数,要显式调用或转化

B.f();//直接说要用派生类的函数f
((B*)p)->f();//把基类指针强制转化成派生类指针

原因就是普通成员函数在静态联编时已经确定。

但是现在,我不要像上面显式调用,而是根据指针指向的对象的类类型来灵活改变调用改对象的函数

此时就要用到虚函数

(1)虚函数的申明

虚函数是在运行时动态确定的(在动态联编时确定)

给函数前面加上virtual称为虚函数,例子如下

class base
{
public:
  virtual void function();
  {cout<<"base"<<endl;}
private:
...
protected:
...
};
class derived1:public base
{
public:
  void function();//派生类中不必加virtual(加也可以)
  {cout<<"derived1"<<endl;}
private:
...
protected:
...
};
class derived2:public base
{
public:
  void function();
  {cout<<"derived2"<<endl;}
private:
...
protected:
...
};

基类中申明了虚函数,派生类中同名的虚函数都会变成虚函数,因此派生类的虚函数声明virtual可以省略,virtual也是在类内声明,类外定义不用加virtual

(2)定义虚函数的注意点
  • 虚函数必须是成员函数,所以不能是自己这类的友元,也不能是静态成员函数

  • 虚函数在子类和派生类中必须一模一样(返回类型,参数,名字一样)

  • 构造函数不能是虚函数,但析构函数可以是虚函数。

为什么

虚函数的调用是在动态编联过程实现的

构造函数是在类创建时调用的,创建类是静态编联过程完成的。

析构函数是在类结束调用的,结束类是程序运行后即动态编联过程完成的。

(3)虚函数的使用

在上面那个例子中

在定义了虚函数之后就可以用基类的指针去调用不同派生类的函数

int main()
{
  base* p_base;//基类指针 p_base
  base obj_base;//基类对象 obj_base
  derived1 obj_der1;//派生类1对象 obj_der1
  derived2 obj_der2;//派生类2对象 obj_der2

  p_base=obj_base;//基类指针 指向基类
  p_base->function();//调用基类函数
  p_base=obj_der1;//基类指针 指向派生类1
  p_base->function();//调用派生类1函数
  p_base=obj_der2;//基类指针 指向派生类2
  p_base->function();//调用派生类2函数
}

程序运行结果

base
dervied1
dervied2

如果在基类中不申明虚函数virtual,那么会全部调用基类的function();

不申明虚函数virtual程序运行结果如下↓

base
base
base
(4)虚析构函数

一般来说,人为用new申请出来的空间变数很多,需要在动态联编时依靠析构函数来删除,从而达到想删除就删除的目的,而不是用到一半就删除了空间。

现在举个例子,有一个基类,又new了一个其派生类的堆,把基类的指针指向派生类的堆。如果delete堆的空间,只会把派生类中从基类继承过来的东西删掉,而派生类自己的成员删不掉,所以要把析构函数申明为virtual。

析构函数申明为virtual之后,会根据指向的对象灵活调用指向对象类的析构函数。

例子,下面不把析构函数说明为虚函数virtual,就会出问题

class A
{
public:
  ~A(){cout<<"A has been deleted"<<endl;}
};
class B:public A
{
public:
  ~B(){cout<<"B has been deleted"<<endl;}
};
int main()
{
  A *p1 =new B;
  delete p1;//B中多余的堆空间没有删掉
  p1=NULL;
  cout<<"--------"<<endl;
  B *p2 =new B;
  delete p2;
  p2=NULL;
}

结果如下:

A has been deleted
--------
B has been deleted
A has been deleted

如果申明为virtual,就不会有问题

class A
{
public:
  virtual ~A(){cout<<"A has been deleted"<<endl;}
};
class B:public A
{
public:
  ~B(){cout<<"B has been deleted"<<endl;}
};

int main()
{
  A *p1 =new B;
  delete p1;
  p1=NULL;
  cout<<"--------"<<endl;
  B *p2 =new B;
  delete p2;
  p2=NULL;
}

结果如下:

B has been deleted
A has been deleted
--------
B has been deleted
A has been deleted

把析构函数申明为virtual,百利而无一害

2、纯虚函数

(1)纯虚函数的定义方法

在基类中

格式: virtual 返回类型 名字(参数表)=0;

以一个例子来说明

virtual int func(int,int)=0;
(2)介绍

纯虚函数就是在不作定义,只作申明的虚函数

前面的虚函数在基类都有定义,是虚函数。如果虚函数在基类不定义就是纯虚函数

纯虚函数在基类不作定义,但是必须在派生类全部有定义,例子如下

游戏 “王者荣耀” 中有很多英雄,他们都属于“英雄类”,共同有生命值(health)这个属性。但是因为防御力不同,受到同等伤害时,扣的血不一样。

class heroes
{
public:
  virtual int reduce_health(int damage)=0;//英雄扣血的纯虚函数
private:
  int health;//生命值
protected:
  ...
};
class LiBai:public heroes//英雄李白的类,继承heroes类
{
public:
  int reduce_health(int damage);//英雄扣血的虚函数
private:
  ...
protected:
  ...
};
class ChengYaojin:public heroes//英雄程咬金的类,继承heroes类
{
public:
  int reduce_health(int damage);//英雄扣血的虚函数
private:
  ...
protected:
  ...
};

int LiBai::reduce_health(int damage)
{
  return 0.8*damage;//受到0.8倍的伤害,即伤害减免20%
}
int ChengYaojin::reduce_health(int damage)
{
  return 0.5*damage;//受到0.5倍的伤害,即伤害减免50%
}

这样,一个heroes类的指针就可以指向多个英雄,调用不同英雄的受伤函数,从而得到扣血的值

3、抽象类

像这样,有纯虚函数的类就是抽象类,比如上面的heroes类,不代表任何一个英雄,代表很多英雄,十分抽象

像LiBai类就是具体类,十分具体地代表了李白这个英雄

一下是几个注意点,反正意思就是说抽象类不能用,要拿其派生类来用

  • 抽象类不能定义对象,只能作为其他类的基类

  • 抽象类不能作为函数参数或返回值等

  • 抽象类的纯虚函数必须在所有具体类中定义,否则未定义该函数的具体类就会成为抽象类

对最后一个进行说明
class heroes
{
public:
  virtual int reduce_health(int damage)=0;//英雄扣血的纯虚函数
private:
  int health;//生命值
protected:
  ...
};
class Assassin:public heroes//英雄刺客的类,继承heroes类,刺客类也是抽象类
{
public:
  ...//没有定义reduce_health,因为不同刺客防御力不一样
private:
  ...
protected:
  ...
};
class LiBai:public Assassin//英雄李白的类,继承刺客类
{
public:
  int reduce_health(int damage);//英雄扣血的虚函数
private:
  ...
protected:
  ...
};

int LiBai::reduce_health(int damage)
{
  return 0.8*damage;//受到0.8倍的伤害,即伤害减免20%
}

刺客类Assassin就是一个抽象类,代表所有刺客,而不代表一个具体的英雄,不定义reduce_health,就会成为抽象类

4、异质链表

(1)指针数组指向相同基类的不同派生类
class heroes
{
public:
  heroes(int hp){health=hp};//初始化英雄生命值
  virtual void print_health()=0//返回生命值
private:
  int health;
};
class LiBai:public heroes
{
public:
  void print_health(){cout<<"李白的生命值为:"<<this->health<<endl};
};
class ChengYaojin:public heroes
{
public:
  void print_health(){cout<<"程咬金的生命值为:"<<this->health<<endl};
};
//...(以下省略其他英雄的定义)
int main()
{
  heroes *h[5];//基类heroes指针数组
  h[0]=new LiBai(1500);//李白类
  h[1]=new ChengYaojin(2500);//程咬金类
  h[2]=new LuBan_7(1200);//鲁班7号类
  h[3]=new HouYi(1300);//后羿类
  h[4]=new XiaoQiao(1000);//小乔类
  for(int i=0;i<5;i++)
  {
    h[i]->print_health();
  }
}

输出结果如下:

李白的生命值为:1500
程咬金的生命值为:2500
鲁班7号的生命值为:1200
后羿的生命值为:1300
小乔的生命值为:1000
(2)动态异质链表

普通链表就是各个结点都是同样的一个类。但是在实际情况中,类中部分内容可能不一样,这就需要异质链表。

异质链表的各个结点都是某基类的派生类,给异质链表开结点,连接结点等操作的指针都是基类指针。

换汤不换药,不再赘述。

模板

一、介绍

(1)为什么要有模板这种东西

某个排序算法申明部分如下:

void sort(int[],int);

十分难受,这个排序算法只支持int整形数组。要是我想对double类型排序,尽管代码是一模一样,只要把数组里段int改成double,但是还是要写一遍。

如果一个一个类型地定义,让一个函数或类普遍使用于多个类型(比如int,double,float三个类型),那么一样的代码要重复写三次,然后把参数改成对应类型。这样很麻烦,操作很重复。

所以我们引入模板这个概念。

(2)模板的好处

程序员只需要定义一次函数或者类,就可以对所有类型的数据进行操作。避免程序员重复冗杂的操作。

(3)函数模板的原理

函数被申明为模板之后,在程序运行到函数被调用时,编译器会根据提供的实参类型,根据模板函数产生专门针对这种类型实参的目标函数——模板函数。从而完成函数调用。

二、函数模板

(1)模板的说明

说明格式如下:

template <typename name_1, typename name_2, typename name_3, ..., typename name_N>

其中name_K(K=1,2,3...)是自定义的名字,每一个typename name_K就是类属参数,代表一个待确定的类型,这个类型在函数被调用时确定

在函数模板中申明和定义时可以将类属参数和普通参数混用

注意:

template <typename name_1, typename name_2, typename name_3, ..., typename name_N>

只对后面紧跟着的那一个函数或者类有效,第二个以其后面的都无效

(2)模板的原理

例子: 某个函数把两个类型相同但未知类型的数相加

template <typename num>

num add(num num1,num num2)//函数模板
{
  num temp;
  temp=num1+num2;
  return temp;
}

在调用函数的时候,由于num被申明为带确定的类型。编译器会根据传入的实参num1和num2来确定类属参数num的类型

比如,如果num1和num2是int类型,编译器就会在整个函数中把num替换为int类型,然后进行处理,此时函数等价于

int add(int num1,int num2)//模板函数1
{
  int temp;
  temp=num1+num2;
  return temp;
}

同理,如果num1和num2是double类型,编译器就会在整个函数中把num替换为double类型,然后进行处理,此时函数等价于

double add(double num1,double num2)//模板函数2
{
  double temp;
  temp=num1+num2;
  return temp;
}

其中待确定的函数称为函数模板,被编译器确定下来的叫模板函数,确定的这个过程叫实例化

类属类型和普通类型可以混用,比如下面的冒泡排序算法:

template <typename ElementType>
void SortBubble(ElementType *a,int size)
{
  int i,work;
  ElementType temp;
  for(int pass=1;pass<size;pass++)
  {
    work=1;
    for(i=0;i<size-pass;i++)
    {
      if(a[i]>a[i+1])
      {
        temp=a[i];
        a[i]=a[i+1];
        a[i+1]=temp;
        work=0;
      }
      if(work!=0)
      {
        break;
      }
    }
  }
}

其中temp是用来交换a[i]和a[i+1]的中间量,由于不知道a[i]的类型,所以用待确定的类型ElementType

当数组首地址作为参数传入形参*a时ElementType同时会被确定为和a[i]相同的类型

注意:相同名字的类属参数在确定类型之后都会被定义为这种类型

比如ElementType被确定为int类,该函数中所有ElementType都变成int

(3)重载函数模板

当函数名字一样时,此时函数进行重载,编译器根据传入参数类型和个数来选择调用哪个函数。

编译器选择调用的函数如下图(在名字相同的前提下)

如果能找到一个能匹配的普通重载函数,那么会优先调用普通的重载函数,不会调用实例化后的模板函数

(4)关于函数模板不能隐式转化

字符和数字之间存在隐式转化关系,在一般情况下,char型变量在赋值给int型变量,或者作为int型参数传入函数的时候都会把char型变量转化为对应的ASCII码

但是在函数模板中这种隐式转化就会出问题,比如

template<typename T>
T Max(const T a, const T b) { return a > b ? a : b; }
//**//
int main()
{
    int k = 1;
    char c = 'c';
    int p=Max(k, c);
    int q=Max(c, k);
    cout << p << endl << q << endl;
}

此时就出错了,模板函数不匹配,T确定下来那就确定了,不能再隐式转换

需要在**处人工加入如下四个中的任意一个。

int Max(const char a, const int b) { return a > b ? a : b; }//#1
int Max(const char a, const char b) { return a > b ? a : b; }//#2
int Max(const int a, const int b) { return a > b ? a : b; }//#3
int Max(const char a, const char b) { return a > b ? a : b; }//#4

结论就是普通的函数,如上面的#1#2#3#4可以隐式转化

模板函数不可以隐式转化

所以如果出现这种情况需要人为重载

三、类模板

(1)类模板说明

类模板说明的方式和函数模板说明类似

比如某个成绩类,某些科目成绩是整数,某些是小数

template<typename T>
class exam
{
public:
  T function();//返回类型为T的函数
private:
...
  T result;//成绩是T类成员
  T *pointer;//T类指针
protected:
...
};

当类中含有类属参数时,该类即为模板类

(2)类模板成员函数的申明与定义

定义方法

template<typename T>
T classname<T>::function()
{
  //定义内容
}
};

再次重申:

只对后面的一个类或者模板有效

所以在函数定义的时候需要加上

template<typename T>

因为定义的是整个模板,而不是某一个特定的数据类型,所以作用域是:

classname<T>::

接着就是很平常的函数声明

如果类属参数不止一个,直接加在template里面

template<typename T1,typename T2,typename T3>
T classname<T1,T2,T3>::function()
{
  //定义内容
}
};

如果是内联函数,直接申明在模板类里面就好,不用写外面一大堆

(3)类模板实例化成为模板类

在主函数中,用以下方法实例化类模板

int main()
{
  classname<int> object1;//实例化为int类并构造对象object1
  classname<double> object2;//实例化为double类并构造对象object2
  classname<float> object3;//实例化为float类并构造对象object3
}

对象的处理和普通的类一样

(4)类模板作为函数参数

函数形参可以是模板类,或模板类的引用。

template<typename T>
void function(classname<T> A, classname<T> &B);//classname<T>是个模板类

传入的是对应类对象,比如

int main()
{
  classname<int> object1;
  classname<double> object2;
  function(object1, object2);//classname<T>是个模板类
}

(5)类模板与类的结构层次

1、基类与派生类

类模板既可以是基类又可以是派生类

  • 类模板作为派生类时,说明自身有基类没有的类属参数
class base//基类
{
public:
  normal_function(T1,T2);
private:
  ...
protected:
};

template<typename T1, typename T2>
class template_classpublic base//模板类继承基类,自己额外定义有类属参数
{
public:
  T1 function(T1,T2);
private:
  T1 private_data;
protected:
  T1 data1;
  T2 data2;
}
  • 类模板作为基类时,派生类继承了实例化后的类
template<typename T1, typename T2>
class template_class//模板类
{
public:
  T1 function(T1,T2);
private:
  T1 private_data;
protected:
  T1 data1;
  T2 data2;
}class derived:public template_class<int>//模板类的派生类,实例化为int类
{
public:
  ...
private:
  ...
protected:
  ...
};
2、类模板与友元

一般的友元就这么申明

template<typename T>
class classname
{
public:
  friend void function();
private:
  ...
protected:
  ...
};

如果友元是函数模板

template<typename T>
class classname
{
public:
  template<typename T>
  friend void function();//友元是函数模板
  template<typename A>
  friend A other_class<T>::f(A, A);//友元是其他类模板的函数模板
  template<typename T>
  friend class other_class;//友元是其他类
private:
  ...
protected:
  ...
};
3、类模板与静态成员

和友元类似,申明static即可

(6)标准模板

标准模板库

c++有很多标准库,其中有一个叫标准模板库(Standard Template Library, STL)

包含了三个主要组件

  • 容器 (container)
  • 迭代器 (iterator)
  • 算法 (algorithm)
1、容器

容器的种类

  • 有序容器 (Sequence Container)
  • 关联容器 (Associative Container)
  • 容器适配器(Container Adapter)
  • 近容器 (Near Container)

近容器比如数组或者string

(由于内容多而杂,自行看书或查阅相关资料)


输入流和输出流


评论
  目录